package IC.SemanticChecks;

import java.util.ArrayList;
import java.util.List;

import IC.SemanticError;
import IC.AST.ArrayLocation;
import IC.AST.Assignment;
import IC.AST.Break;
import IC.AST.CallStatement;
import IC.AST.Continue;
import IC.AST.Expression;
import IC.AST.ExpressionBlock;
import IC.AST.Field;
import IC.AST.Formal;
import IC.AST.ICClass;
import IC.AST.If;
import IC.AST.Length;
import IC.AST.LibraryMethod;
import IC.AST.Literal;
import IC.AST.LocalVariable;
import IC.AST.LogicalBinaryOp;
import IC.AST.LogicalUnaryOp;
import IC.AST.MathBinaryOp;
import IC.AST.MathUnaryOp;
import IC.AST.Method;
import IC.AST.NewArray;
import IC.AST.NewClass;
import IC.AST.PrimitiveType;
import IC.AST.Program;
import IC.AST.Return;
import IC.AST.Statement;
import IC.AST.StatementsBlock;
import IC.AST.StaticCall;
import IC.AST.StaticMethod;
import IC.AST.This;
import IC.AST.UserType;
import IC.AST.VariableLocation;
import IC.AST.VirtualCall;
import IC.AST.VirtualMethod;
import IC.AST.Visitor;
import IC.AST.While;
import IC.Symbols.BlockSymbolTable;
import IC.Symbols.ClassSymbol;
import IC.Symbols.FieldSymbol;
import IC.Symbols.GlobalSymbolTable;
import IC.Symbols.Kind;
import IC.Symbols.MethodSymbol;
import IC.Symbols.MethodSymbolTable;
import IC.Symbols.Symbol;
import IC.Symbols.SymbolTable;
import IC.Types.ArrayType;
import IC.Types.ClassType;
import IC.Types.MethodType;
import IC.Types.Type;
import IC.Types.TypeTable;

public class ScopeCheckingVisitor implements Visitor {
	private GlobalSymbolTable gst;

	
	@Override
	public Object visit(Program program) {
		gst = (GlobalSymbolTable) program.getEnclosingScope();

		for (ICClass icClass: program.getClasses()){
			if (icClass.accept(this) == null){
				return null;
			}
		}
		return true;
	}

	@Override
	public Object visit(ICClass icClass) {
		//with fields, we assume the getFileds has unique enries (treated in the tables builder)
		//the only thing left to check is in case where superClass exists - no hiding allowed
		for (Field field : icClass.getFields()){
			if (icClass.hasSuperClass() && (icClass.getEnclosingScope().lookup(field.getName()) != null)){
				System.err.println(new SemanticError(icClass.getLine(), "Same field as Super's "));
				return null;
			}
		}

		for (Method method : icClass.getMethods()){
			Symbol currentMethodSymbol = method.getEnclosingScope().lookup(method.getName());

			//checks if there exists the same symbol in the parent scope (superClass or Global). null if not exist.
			Symbol symbolInParent = method.getEnclosingScope().getParent().lookup(method.getName());

			if (symbolInParent != null){ //exists

				if ((symbolInParent.getKind() == Kind.STATIC_METHOD) || (symbolInParent.getKind() == Kind.VIRTUAL_METHOD)){
					//parent symbol is also a method - must be the same kind
					if ((currentMethodSymbol.getKind() == Kind.STATIC_METHOD) && (symbolInParent.getKind() == Kind.VIRTUAL_METHOD)) {
						System.err.println(new SemanticError(method.getLine(), "Can't override - Static method has the same name as Virtual in super"));
						return null;
					} else if ((currentMethodSymbol.getKind() == Kind.VIRTUAL_METHOD) && (symbolInParent.getKind() == Kind.STATIC_METHOD)) {
						System.err.println(new SemanticError(method.getLine(), "Can't override - Virtual method has the same name as Static in super"));
						return null;
					} else if (symbolInParent.getType() != currentMethodSymbol.getType()){
						// parent symbol is the same kind - must have the same signature.
						System.err.println(new SemanticError(method.getLine(), "overload is not allowed in IC"));
						return null;	
					}
				} else if (symbolInParent.getKind() == Kind.FIELD) { 
					//parent symbol is a field - cant override
					System.err.println(new SemanticError(method.getLine(), "can't override identifiers"));
					return null;
				}
			}
			if (method.accept(this) == null){
				return null;
			}
		}
		return true;
	}

	@Override
	public Object visit(Field field) {
		return true;
	}

	@Override
	public Object visit(VirtualMethod method) {
		for (Statement statement : method.getStatements()){
			if (statement.accept(this) == null){
				return null;
			}
		}
		return true;
	}

	@Override
	public Object visit(StaticMethod method) {
		for (Statement statement : method.getStatements()){
			if (statement.accept(this) == null){
				return null;
			}
		}
		return true;
	}

	@Override
	public Object visit(LibraryMethod method) {
		return true;
	}

	@Override
	public Object visit(Formal formal) {
		return true;
	}

	@Override
	public Object visit(PrimitiveType type) {
		return true;
	}

	@Override
	public Object visit(UserType type) {
		return true;
	}

	@Override
	public Object visit(Assignment assignment) {
		if ((assignment.getVariable().accept(this) == null) || (assignment.getAssignment().accept(this) == null)) {
			return null;
		}
		return true;
	}

	@Override
	public Object visit(CallStatement callStatement) {
		if (callStatement.getCall().accept(this)== null){
			return null;		
		}
		return true;
	}

	@Override
	public Object visit(Return returnStatement) {
		if (returnStatement.hasValue() && (returnStatement.getValue().accept(this) == null)) {
			return null;
		}
		return true;
	}

	@Override
	public Object visit(If ifStatement) {
		if ((ifStatement.getCondition().accept(this) == null) || (ifStatement.getOperation().accept(this) == null)) {
			return null;
		} 
		if (ifStatement.hasElse() && (ifStatement.getElseOperation().accept(this) == null)) {
			return null; 
		}
		return true;
	}

	@Override
	public Object visit(While whileStatement) {
		if ((whileStatement.getCondition().accept(this) == null) || (whileStatement.getOperation().accept(this) == null)) {
			return null;
		}
		return true;
	}

	@Override
	public Object visit(Break breakStatement) {
		return true;
	}

	@Override
	public Object visit(Continue continueStatement) {
		return true;
	}

	@Override
	public Object visit(StatementsBlock statementsBlock) {
		for (Statement statement : statementsBlock.getStatements()){
			if (statement.accept(this) == null){
				return null;
			}
		}
		return true;
	}

	@Override
	public Object visit(LocalVariable localVariable) {
		if (localVariable.hasInitValue() && (localVariable.getInitValue().accept(this) == null)){
			return null;
		}
		return true;
	}

	@Override
	public Object visit(VariableLocation location) {
		if (!location.isExternal()){
			//variable is not external. check if exist in current scope
			Symbol symbol = location.getEnclosingScope().lookup(location.getName(), location.getOrder());
			if (symbol == null ){
				System.err.println(new SemanticError(location.getLine(), location.getName() + " was not declared"));
				return null;
			}

			if (symbol instanceof FieldSymbol){
				boolean isStaticScope = isEnclosingMethodScopeStatic(location.getEnclosingScope());
				if (isStaticScope){
					System.err.println(new SemanticError(location.getLine(), "Cannot reference a field \"" + location.getName() + "\" from a static scope"));
					return null;
				}
			}
			if (symbol.getType() instanceof ClassType){
				// variable is class. we need its scope to determine its methods/fields
				return getClassSymbolTable((ClassType)symbol.getType());
				//return ((ClassType)symbol.getType()).getAst().getEnclosingScope();
			} else if (symbol.getType() instanceof ArrayType){
				//variable is array. we need the scope of its content type if its classType
				Type arrayType = symbol.getType();
				while (arrayType instanceof ArrayType){
					arrayType = ((ArrayType) arrayType).getElementType();
				}

				if (arrayType instanceof ClassType){
					return getClassSymbolTable((ClassType) arrayType);
				}

				//we need to return an empty symbol table
				return new BlockSymbolTable("", null);
			}
		} else {
			//variable is external - find it exists
			Object res = location.getLocation().accept(this);
			if (!(res instanceof SymbolTable)){
				System.err.println(new SemanticError(location.getLine(),"field " + location.getName() + " was not found"));
				return null;
			}
			SymbolTable locationSymbolTable = (SymbolTable) res;
			//location is an object. it must be an object fields since static field is not allowed
			Symbol fieldSymbol = locationSymbolTable.lookup(location.getName());
			if (fieldSymbol == null){
				//field wasn't found
				System.err.println(new SemanticError(location.getLine(),"field " + location.getName() + " was not found"));
				return null;
			} else if (fieldSymbol.getType() instanceof ClassType){
				return getClassSymbolTable((ClassType)fieldSymbol.getType());
			} else if (fieldSymbol.getType() instanceof ArrayType){
				Type arrayType = (ArrayType)fieldSymbol.getType();

				while (arrayType instanceof ArrayType){
					arrayType = ((ArrayType)arrayType).getElementType();
				}
				if (arrayType instanceof ClassType){
					return getClassSymbolTable((ClassType)arrayType);					
				}
			}
		}
		return true;
	}

	@Override
	public Object visit(ArrayLocation location) {

		if (location.getIndex().accept(this) == null){
			return null;
		}		
		return location.getArray().accept(this);
	}

	@Override
	public Object visit(StaticCall call) {
		SymbolTable symbolTableToSearch;
		Symbol classSymbol = gst.lookup(call.getClassName()); 

		if (classSymbol == null){
			//class not found
			System.err.println(new SemanticError(call.getLine(), "class " + call.getClassName() + " was not found"));
			return null;
		} else {
			//if found - obviously a class. get its scope and search for the method
			symbolTableToSearch = getClassSymbolTable((ClassType)((ClassSymbol)classSymbol).getType());
			Symbol methodSymbol = symbolTableToSearch.lookup(call.getName());
			if (methodSymbol == null){
				//no such symbol
				System.err.println(new SemanticError(call.getLine(), "static method " + call.getName() + " was not found"));
				return null;	
			} else {
				//symbol was found - must be of StaticMehod kind
				if (methodSymbol.getKind() != Kind.STATIC_METHOD){
					System.err.println(new SemanticError(call.getLine(), "static method " + call.getName() + " was not found"));
					return null;
				}
			}

			for (Expression argument : call.getArguments()){
				if (argument.accept(this) == null){
					return null;
				} 
			}

			//must be MethodType - checking anyway
			if (methodSymbol.getType() instanceof MethodType){
				MethodType methodType = (MethodType)methodSymbol.getType();
				if (methodType.getReturnType() instanceof ClassType){
					return ((ClassType)(methodType.getReturnType())).getAst().getEnclosingScope();
				}
			}
		}
		return true;
	}

	@Override
	public Object visit(VirtualCall call) {

		SymbolTable symbolTableToSearch;
		MethodSymbol foundSymbol;

		if (call.isExternal()){
			//method is external. need to get the correct object scope
			symbolTableToSearch = (SymbolTable)call.getLocation().accept(this);
		} else {
			//method is in called from an object. need to get the current scope
			symbolTableToSearch = call.getEnclosingScope();
		}

		if (symbolTableToSearch == null) {
			//class was not found
			System.err.println(new SemanticError(call.getLine(), "virtual method " + call.getName() + " was not found"));
			return null;
		} else {
			foundSymbol = (MethodSymbol) symbolTableToSearch.lookupMethod(call.getName());
			//foundSymbol = (MethodSymbol) symbolTableToSearch.lookup(call.getName());
			if ((foundSymbol == null)){
				//virtual method inside object wasn't found
				System.err.println(new SemanticError(call.getLine(), "virtual method " + call.getName() + " was not found"));
				return null;
			}

			if (!foundSymbol.isStatic() && !(call.isExternal())){ //A non external virtual call and a non external static method are parsed the same but we need to take care of this
				boolean isStaticScope = isEnclosingMethodScopeStatic(call.getEnclosingScope());
				if (isStaticScope){
					System.err.println(new SemanticError(call.getLine(), "Cannot invoke a virtual method from a static scope"));
					return null;
				}
			}
		}

		for (Expression argument : call.getArguments()){
			if (argument.accept(this) == null){
				return null;
			} 
		}

		if (foundSymbol.getType() instanceof MethodType){
			MethodType methodType = (MethodType)foundSymbol.getType();
			if (methodType.getReturnType() instanceof ClassType){
				return ((ClassType)(methodType.getReturnType())).getAst().getEnclosingScope();
			}
		}

		return true;
	}

	@Override
	public Object visit(This thisExpression) {
		return thisExpression.getEnclosingScope();
	}

	@Override
	public Object visit(NewClass newClass) {
		return getClassSymbolTable(TypeTable.classType(newClass.getName()));
	}

	@Override
	public Object visit(NewArray newArray) {
		if ((newArray.getSize().accept(this) == null) || (newArray.getType().accept(this) == null)){
			return null;
		}
		return true;
	}

	@Override
	public Object visit(Length length) {
		if (length.getArray().accept(this) == null){
			return null;
		}
		return true;
	}

	@Override
	public Object visit(MathBinaryOp binaryOp) {
		if ((binaryOp.getFirstOperand().accept(this) == null) || (binaryOp.getSecondOperand().accept(this) == null)){
			return null;
		}
		return true;
	}

	@Override
	public Object visit(LogicalBinaryOp binaryOp) {
		if ((binaryOp.getFirstOperand().accept(this) == null) || (binaryOp.getSecondOperand().accept(this) == null)){
			return null;
		}
		return true;
	}

	@Override
	public Object visit(MathUnaryOp unaryOp) {
		if (unaryOp.getOperand().accept(this) == null){
			return null;
		}
		return true;
	}

	@Override
	public Object visit(LogicalUnaryOp unaryOp) {
		if (unaryOp.getOperand().accept(this) == null){
			return null;
		}
		return true;
	}

	@Override
	public Object visit(Literal literal) {
		return true;
	}

	@Override
	public Object visit(ExpressionBlock expressionBlock) {
		return expressionBlock.getExpression().accept(this);
	}

	private SymbolTable getClassSymbolTable(ClassType classType){

		List<String> classHierarchy = new ArrayList<String>();

		ClassType currentType = classType;
		classHierarchy.add(currentType.getName());
		String superClassName = currentType.getSuperClassName();

		while (superClassName != null){
			classHierarchy.add(superClassName);
			currentType = currentType.getSuperClass();
			superClassName = currentType.getSuperClassName();
		}

		SymbolTable currentSymbolTable = gst;
		for (int i = classHierarchy.size() - 1; i >= 0; i--){
			currentSymbolTable = currentSymbolTable.getChild(classHierarchy.get(i));
		}
		return currentSymbolTable;
	}

	private boolean isEnclosingMethodScopeStatic(SymbolTable symbolTableToCheck){
		SymbolTable tempSymbolTable = symbolTableToCheck;
		while ((tempSymbolTable != null) && !(tempSymbolTable instanceof MethodSymbolTable)){
			tempSymbolTable = tempSymbolTable.getParent();
		}

		if (tempSymbolTable == null){
			return false;
		}

		if (((MethodSymbolTable) tempSymbolTable).isStatic()){
			return true;
		}
		return false;
	}

}
